﻿//
// ArtistLibraryUpdater.cs: Worker that updates the library from files.
//
// Copyright 2010 Michael Goethe
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using TagLib;

namespace S9PalCommon.ArtistLibrary
{
    /// <summary>
    /// Worker that updates the library from files. Gets tag data as well as images.
    /// </summary>
    public class ArtistLibraryUpdater : Worker
    {
        #region Fields

        const String c_albumArtFileNames = "cover*.jpg; cover*.jpeg; folder*.jpg; folder*.jpeg; *.jpg";
        const String c_audioFileTypes = "*.mp3; *.flac; *.wma; *.ogg; *.ape";

        private SortedList<String, Artist> artists = new SortedList<String, Artist>();
        SortedList<String, Artist> artistsFound = new SortedList<String, Artist>();
        private String path;
        private int startResumeIndex = 0;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates an ArtistLibraryUpdater object, which updates the library from a path of files.
        /// </summary>
        /// <param name="artistLibrary">The library to update.</param>
        /// <param name="path">The path to search for files.</param>
        public ArtistLibraryUpdater(Library artistLibrary, String path)
        {
            artists = artistLibrary.Artists;
            this.path = path;

            Name = "Artist Library Updater";
            ItemCategory = "File";
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Begins the importing of tag data and images from the files.
        /// </summary>
        protected override void OnStart()
        {
            String[] files;
            TagLib.File audioFile = null;
            Tag audioTag = null;
            String artistName;
            String albumName;
            byte[] picData;
            uint albumYear;
            Artist artist;                                          // Current artist.
            Artist previousArtist = null;                           // Previous artist scanned. Included for efficiency purposes.
            Album album;                                            // Current album.
            String currentDirectory;                                // Current directory that's being searched through.
            String previousDirectory = null;                        // Previous directory that was searched. Used so we don't search for photos in a directory more than once.

            // Get the paths of the matching audio files.
            files = S9PalCommon.Helper.GetFilesWithMultiplePatterns(path, c_audioFileTypes, SearchOption.AllDirectories);

            // Update the total number of items. This will define the end-point for the process, allowing us to give the user a good measure of progress.
            ItemCount = files.Length;

            for (int i = startResumeIndex; i < files.Length; i++)
            {
                // If the user is trying to stop the worker, then stop it.
                if (Status == WorkerStatus.Stopping)
                {
                    Status = WorkerStatus.Stopped;
                    return;
                }
                if (Status == WorkerStatus.Pausing)
                {
                    startResumeIndex = i;
                    Status = WorkerStatus.Paused;
                    return;
                }

                // Set the picData to null on each iteration so we don't get any images carrying over from artist to artist or album to album.
                picData = null;

                // Update the current item name and number. This will help let the user know how far along the updater is.
                CurrentItemName = Path.GetFileName(files[i]);
                CurrentItemNumber = i + 1;

                // Get the audio file.
                try
                {
                    audioFile = TagLib.File.Create(files[i]);
                }
                catch (DirectoryNotFoundException)
                {
                    // If we've lost connection with the device, then stop it. Otherwise, log the error.
                    if (Syncer.Drive == null)
                        Stop();
                    else
                        Global.Logger.Error("Could not find directory: " + Path.GetDirectoryName(files[i]));

                    continue;
                }
                catch (FileNotFoundException)
                {
                    // If we've lost connection with the device, then stop it. Otherwise, log the error.
                    if (Syncer.Drive == null)
                        Stop();
                    else
                        Global.Logger.Error("Could not find file: " + Path.GetFileName(files[i]));

                    continue;
                }
                catch (CorruptFileException)
                {
                    Global.Logger.Error("File appears to be corrupt: " + Path.GetFileName(files[i]));
                    continue;
                }
                catch (UnsupportedFormatException)
                {
                    Global.Logger.Error("File format is unsupported: " + Path.GetFileName(files[i]));
                    continue;
                }

                // Attempt to get the file's tag info.
                if (audioFile.TagTypes.HasFlag(TagTypes.Id3v2))
                    audioTag = audioFile.GetTag(TagTypes.Id3v2);
                else if (audioFile.TagTypes.HasFlag(TagTypes.Id3v1))
                    audioTag = audioFile.GetTag(TagTypes.Id3v1);
                else if (audioFile.TagTypes.HasFlag(TagTypes.Asf))
                    audioTag = audioFile.GetTag(TagTypes.Asf);
                else if (audioFile.TagTypes.HasFlag(TagTypes.Xiph))
                    audioTag = audioFile.GetTag(TagTypes.Xiph);
                else if (audioFile.TagTypes.HasFlag(TagTypes.FlacMetadata))
                    audioTag = audioFile.GetTag(TagTypes.FlacMetadata);
                else
                    continue;

                // Attempt to get the artist name. If the file isn't tagged properly, however, it might fail, so let's account for that.
                // Also make sure to trim leading and trailing whitespace.
                try
                {
                    artistName = audioTag.FirstPerformer;
                    if (artistName != null)
                        artistName = artistName.Trim();
                }
                catch (IndexOutOfRangeException)
                {
                    // This isn't critical. Log the error and move on.
                    Global.Logger.Error("Error getting artist name in file: " + Path.GetFileName(files[i]));
                    continue;
                }

                // If no artist name was found, don't bother trying to add the artist.
                if (String.IsNullOrWhiteSpace(S9PalCommon.Helper.RemoveInvalidFileNameCharacters(artistName)))
                {
                    // Again not critical to the continuation of the application. Log and move on.
                    Global.Logger.Warn("No artist name found in file: " + Path.GetFileName(files[i]));
                    continue;
                }

                // If the current artist key isn't in the library, add the new artist.
                if (!artists.ContainsKey(artistName))
                    artists.Add(artistName, new Artist(artistName));
                
                // Get the current artist (whether it was just added in the line above or has been there).
                artist = artists[artistName];

                // Add artist to the artistsFound list, so we can later remove artists not found.
                if (artist != previousArtist)
                {
                    if (!artistsFound.ContainsKey(artistName))
                        artistsFound.Add(artistName, artist);
                    previousArtist = artist;
                }

                // Get the album name and album year.
                // Also make sure to trim leading and trailing whitespace.
                albumName = audioTag.Album;
                if (albumName != null)
                    albumName = albumName.Trim();
                albumYear = audioTag.Year;

                // If no album is listed, don't bother trying to add the album.
                if (String.IsNullOrWhiteSpace(S9PalCommon.Helper.RemoveInvalidFileNameCharacters(albumName)))
                {
                    // Again not critical to the continuation of the application. Log and move on.
                    Global.Logger.Warn("No album name found in file: " + Path.GetFileName(files[i]));
                    continue;
                }

                // If the album key isn't in the library, add the new album.
                if (!artist.Albums.ContainsKey(albumName))
                    artist.Albums.Add(albumName, new Album(albumName, albumYear));

                // Get the current album (whether it was just added or has been there).
                album = artist.Albums[albumName];

                // Get any pictures that are in the file.
                for (int j = 0; j < audioTag.Pictures.Length; j++)
                {
                    picData = audioTag.Pictures[j].Data.ToArray<byte>();

                    // Import album art from picture data if found in the tag.
                    if (picData != null && picData.Length > 0)
                        ImportAlbumArtFromPictureData(picData, artist, album);
                }

                // If we're looking at a directory different from what we were previously, then import album art from the directory where the audio file is located.
                currentDirectory = Path.GetDirectoryName(files[i]);
                if (currentDirectory.CompareTo(previousDirectory) != 0)
                {
                    ImportAlbumArtFromDirectory(files[i].Remove(files[i].LastIndexOf("\\")), c_albumArtFileNames, artist, album);
                    previousDirectory = currentDirectory;
                }
            }

            // Clean up artists that no longer have been found.
            RemoveMissingArtists(artistsFound);

            // TO DO: Add clean-up for albums. And going on into the future, probably each individual file.

            if (Status == WorkerStatus.Active)
            {
                S9PalCommon.Global.ArtistLibrary.SaveLibrary();
                Status = WorkerStatus.Finished;
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Given the MD5 hash value of the picture data, this method checks if the image already exists for an artist and album.
        /// </summary>
        /// <param name="hashValue">Hash value of the picture data.</param>
        /// <param name="artist">Artist to check.</param>
        /// <param name="album">Album to check.</param>
        /// <returns></returns>
        private bool DoesAlbumArtExist(String hashValue, Artist artist, Album album)
        {
            // Check to see if there is already an AlbumArt with a matching hash value within the current Album.
            foreach (AlbumArt aa in artist.Albums[album.Name].AlbumArt)
            {
                if (hashValue.CompareTo(aa.HashValue) == 0)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Imports album art from a directory by first searching the directory and then copying matching files over to the library directory.
        /// </summary>
        /// <param name="directoryToSearch">The directory to search.</param>
        /// <param name="albumArtFileNames">File names to match and copy. Multiple patterns are allowed and should be separated with semi-colons.</param>
        /// <param name="artist">Artist to import the image to.</param>
        /// <param name="album">Album to import the image to.</param>
        private void ImportAlbumArtFromDirectory(String directoryToSearch, String albumArtFileNames, Artist artist, Album album)
        {
            String[] filePaths;
            DirectoryInfo albumArtDir;
            String hashValue;

            // Compose the directory path. Create the directory if it doesn't exist.
            albumArtDir = new DirectoryInfo(S9PalCommon.Global.ArtistLibrary.LibraryDirectory + "\\" + Helper.RemoveInvalidFileNameCharacters(artist.Name) + "\\" + Helper.RemoveInvalidFileNameCharacters(album.Name));
            if (!albumArtDir.Exists) albumArtDir.Create();

            // Load the matching file paths into the filePaths array.
            filePaths = S9PalCommon.Helper.GetFilesWithMultiplePatterns(directoryToSearch, albumArtFileNames, SearchOption.TopDirectoryOnly);

            for (int i = 0; i < filePaths.Length; i++)
            {
                hashValue = S9PalCommon.Helper.GetHashValue(System.IO.File.ReadAllBytes(filePaths[i]));

                // If the image isn't already in the library, copy it to the library and assign a new AlbumArt object.
                if (!DoesAlbumArtExist(hashValue, artist, album))
                {
                    String destPath = albumArtDir.ToString() + "\\" + S9PalCommon.Global.ArtistLibrary.AlbumArtPrefix + hashValue + S9PalCommon.Global.ArtistLibrary.AlbumArtExtension;

                    System.IO.File.Delete(destPath);
                    try
                    {
                        System.IO.File.Copy(filePaths[i], destPath, true);
                    }
                    catch (DirectoryNotFoundException ex)
                    {
                        // If we've lost connection with the device, then stop it. Otherwise, log the error.
                        if (Syncer.Drive == null)
                            Stop();
                        else
                            Global.Logger.Error(ex.Message);

                        continue;
                    }
                    artist.Albums[album.Name].AlbumArt.Add(new AlbumArt(hashValue));
                }
            }
        }

        /// <summary>
        /// Imports album art from a byte array of picture data, and saves it to a file using its hash value as the differentiating portion of the file name.
        /// </summary>
        /// <param name="picData">The picture data to import.</param>
        /// <param name="artist">Artist to import the image to.</param>
        /// <param name="album">Album to import the image to.</param>
        /// <param name="filePath">Just in case there's a problem getting the picture data we want to be able to report the problematic file.</param>
        private void ImportAlbumArtFromPictureData(byte[] picData, Artist artist, Album album)
        {
            DirectoryInfo albumArtDir;
            String hashValue = S9PalCommon.Helper.GetHashValue(picData);

            // Compose the directory path. Create the directory if it doesn't exist.
            albumArtDir = new DirectoryInfo(S9PalCommon.Global.ArtistLibrary.LibraryDirectory + "\\" + Helper.RemoveInvalidFileNameCharacters(artist.Name) + "\\" + Helper.RemoveInvalidFileNameCharacters(album.Name));
            if (!albumArtDir.Exists) albumArtDir.Create();
            
            if (!DoesAlbumArtExist(hashValue, artist, album))
            {
                try
                {
                    using (MemoryStream stream = new MemoryStream(picData))
                    {
                        using (Image image = Image.FromStream(stream))
                        {
                            image.Save(albumArtDir.ToString() + "\\" + S9PalCommon.Global.ArtistLibrary.AlbumArtPrefix + hashValue + S9PalCommon.Global.ArtistLibrary.AlbumArtExtension, ImageFormat.Jpeg);
                        }
                    }
                    artist.Albums[album.Name].AlbumArt.Add(new AlbumArt(hashValue));
                }
                catch (ArgumentException)
                {
                    // This exception will get thrown if the picture data contained in picData is invalid.
                    // Nothing too critical, log it and move on.
                    Global.Logger.Error("The picture data contained in the following file appears to be invalid: " + CurrentItemName);
                }
            }
        }

        /// <summary>
        /// Removes all artists other than those that were found.
        /// </summary>
        /// <param name="artistsFound">The artists that were found in the files searched.</param>
        private void RemoveMissingArtists(SortedList<String, Artist> artistsFound)
        {
            SortedList<String, Artist> artistsToRemove = new SortedList<String, Artist>();

            // Add artists that are no longer found in the user's files to artistsToRemove.
            foreach (KeyValuePair<String, Artist> artistKVPair in artists)
            {
                if (!artistsFound.ContainsKey(artistKVPair.Key))
                    artistsToRemove.Add(artistKVPair.Key, artistKVPair.Value);
            }

            // Remove any artists that the user no longer has files for.
            foreach (KeyValuePair<String, Artist> artistKVPair in artistsToRemove)
                artists.Remove(artistKVPair.Key);
        }

        #endregion
    }
}
